﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
using GraphLight.Controls;
using GraphLight.Graph;
using GraphLight.ViewModel;

namespace GraphLight.Drawing
{
    public class GraphControl : Control
    {
        #region Private fields

        private readonly IDictionary<object, FrameworkElement>
            _elementMap = new Dictionary<object, FrameworkElement>();
        private readonly GraphVizLayout _layout;
        private readonly GraphViewModel _viewModel;
        private readonly DummyNodeMeasure _measure;
        private Canvas _graphItems;
        private Grid _mainGrid;
        private bool _isLoaded;
        private ScrollViewer _scrollViewer;
        private readonly DispatcherTimer _timer = new DispatcherTimer();

        #endregion

        #region Initialization

        public GraphControl()
        {
            _measure = new DummyNodeMeasure();
            _layout = new GraphVizLayout { NodeMeasure = _measure };
            _viewModel = new GraphViewModel();
            LayoutCommand = new DelegateCommand(Layout);
            DefaultStyleKey = typeof(GraphControl);
            Loaded += onLoaded;
            Unloaded += onUnloaded;
            _timer.Interval = TimeSpan.FromMilliseconds(500);
            _timer.Tick += onTimerTick;
        }

        void onTimerTick(object sender, EventArgs e)
        {
            _timer.Stop();
            clearAllItems();
            shift();
            fillVerteces();
            fillEdges();
        }

        void onUnloaded(object sender, RoutedEventArgs e)
        {
            _isLoaded = false;
            clearAllItems();
        }

        void onLoaded(object sender, RoutedEventArgs e)
        {
            if (!_isLoaded)
            {
                _isLoaded = true;
                Layout();
            }
            else
            {
                fillVerteces();
                fillEdges();
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _mainGrid = getTemplateChild<Grid>("mainGrid");
            _graphItems = getTemplateChild<Canvas>("graphCanvas");
            _scrollViewer = getTemplateChild<ScrollViewer>("scrollViewer");
            var layoutRoot = getTemplateChild<FrameworkElement>("LayoutRoot");
            layoutRoot.DataContext = _viewModel;
            _mainGrid.MouseMove += onMouseMove;
            _mainGrid.MouseLeftButtonDown += onMouseLeftButtonDown;
            _mainGrid.MouseLeftButtonUp += onMouseLeftButtonUp;
            //_mainGrid.KeyUp += onKeyUp;
            AddHandler(KeyUpEvent, new KeyEventHandler(onKeyUp), true);
            _scrollViewer.SizeChanged += onSizeChanged;

            DragDropManager.AddDropQueryHandler(this, _viewModel.OnDropQuery);
            DragDropManager.AddDropInfoHandler(this, _viewModel.OnDropInfo);
            DragDropManager.AddDragQueryHandler(this, _viewModel.OnDragQuery);
        }

        void onSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (!_isLoaded || Graph == null)
                return;
            _timer.Stop();
            _timer.Start();
        }

        #endregion

        #region API

        public void Layout()
        {
            if (!_isLoaded || Graph == null)
                return;
            clearAllItems();
            fillVerteces();
            _layout.Graph = Graph;
            _layout.NodeMeasure = _measure;
            _layout.Layout();
            shift();
            fillEdges();
        }

        private void shift()
        {
            var viewWidth = _scrollViewer.ViewportWidth - 1;
            var viewHeight = _scrollViewer.ViewportHeight - 1;
            if (double.IsNaN(viewWidth) || double.IsNaN(viewHeight))
                return;

            var minX = double.MaxValue;
            var maxX = double.MinValue;
            var minY = double.MaxValue;
            var maxY = double.MinValue;
            foreach (var vertex in Graph.Verteces)
            {
                if (vertex.Data.Left < minX)
                    minX = vertex.Data.Left;
                if (vertex.Data.Right > maxX)
                    maxX = vertex.Data.Right;
                if (vertex.Data.Top < minY)
                    minY = vertex.Data.Top;
                if (vertex.Data.Bottom > maxY)
                    maxY = vertex.Data.Bottom;
            }
            var graphWidth = maxX - minX;
            var graphHeight = maxY - minY;

            var leftSpace = graphWidth < viewWidth
                ? (viewWidth - graphWidth) / 2
                : 0.0;

            var topSpace = (graphHeight < viewHeight)
                ? (viewHeight - graphHeight) / 2
                : 0.0;

            var leftShift = leftSpace - minX;
            var topShift = topSpace - minY;

            if (leftShift != 0 || topShift != 0)
            {
                foreach (var vertex in Graph.Verteces)
                {
                    vertex.Data.Left += leftShift;
                    vertex.Data.Top += topShift;
                }
                foreach (DrawingEdge edge in Graph.Edges)
                {
                    foreach (var point2D in edge.Data.DraggablePoints)
                    {
                        point2D.X += leftShift;
                        point2D.Y += topShift;
                    }
                }

                Graph.Width = Math.Max(graphWidth, viewWidth);
                Graph.Height = Math.Max(graphHeight, viewHeight);
            }
        }

        #endregion

        #region Graph

        public static readonly DependencyProperty GraphProperty =
            DependencyProperty.Register("Graph", typeof(DrawingGraph), typeof(GraphControl),
                new PropertyMetadata(onGraphChanged));

        public DrawingGraph Graph
        {
            get { return (DrawingGraph)GetValue(GraphProperty); }
            set { SetValue(GraphProperty, value); }
        }

        private static void onGraphChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var oldVal = (DrawingGraph)e.OldValue;
            var newVal = (DrawingGraph)e.NewValue;
            var control = (GraphControl)d;
            control.onGraphChanged(oldVal, newVal);
        }

        private void onGraphChanged(DrawingGraph oldVal, DrawingGraph newVal)
        {
            _viewModel.Graph = newVal;

            if (oldVal != null)
            {
                var edges = oldVal.Edges as INotifyCollectionChanged;
                var verteces = oldVal.Verteces as INotifyCollectionChanged;
                if (edges != null)
                    edges.CollectionChanged -= onEdgesCollectionChanged;
                if (verteces != null)
                    verteces.CollectionChanged -= onVertexCollectionChanged;
                clearAllItems();
            }

            if (newVal != null)
            {
                var edges = newVal.Edges as INotifyCollectionChanged;
                var verteces = newVal.Verteces as INotifyCollectionChanged;
                if (edges != null)
                    edges.CollectionChanged += onEdgesCollectionChanged;
                if (verteces != null)
                    verteces.CollectionChanged += onVertexCollectionChanged;
                Layout();
            }
        }

        #endregion

        #region LayoutCommand

        public static readonly DependencyProperty LayoutCommandProperty =
            DependencyProperty.Register("LayoutCommand", typeof(ICommand), typeof(GraphControl), null);

        public ICommand LayoutCommand
        {
            get { return (ICommand)GetValue(LayoutCommandProperty); }
            private set { SetValue(LayoutCommandProperty, value); }
        }

        #endregion

        #region Graph event handlers

        private void onMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (_viewModel.SelectedEditorTool != null)
                _viewModel.SelectedEditorTool.HandleLButtonDown(sender, e, _graphItems);
        }

        private void onMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (_viewModel.SelectedEditorTool != null)
                _viewModel.SelectedEditorTool.HandleLButtonUp(sender, e);
        }

        private void onMouseMove(object sender, MouseEventArgs e)
        {
            if (_viewModel.SelectedEditorTool != null)
                _viewModel.SelectedEditorTool.HandleMouseMove(sender, e, _graphItems);
        }

        private void onKeyUp(object sender, KeyEventArgs e)
        {
            if (_viewModel.SelectedEditorTool != null)
                _viewModel.SelectedEditorTool.HandleKeyUp(sender, e);
        }

        #endregion

        #region Private methods

        private void onVertexCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
                foreach (var vertex in e.NewItems.OfType<IVertex<VertexAttrs, EdgeAttrs>>())
                    addVertex(vertex);
            if (e.OldItems != null)
                foreach (var vertex in e.OldItems)
                    delItem(vertex);
        }

        private void onEdgesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
                foreach (var edge in e.NewItems.OfType<IEdge<VertexAttrs, EdgeAttrs>>())
                    addEdge(edge);
            if (e.OldItems != null)
                foreach (var edge in e.OldItems)
                    delItem(edge);
        }

        private void addEdge(IEdge<VertexAttrs, EdgeAttrs> edge)
        {
            var presenter = new Edge { Content = edge, DataContext = edge };
            presenter.SetBinding(Canvas.ZIndexProperty, new Binding("Data.ZIndex"));
            presenter.SetBinding(Edge.IsSelectedProperty, new Binding("Data.IsSelected"));
            presenter.SetBinding(Edge.IsHighlightedProperty, new Binding("Data.IsHighlighted"));

            _graphItems.Children.Add(presenter);
            _elementMap.Add(edge, presenter);
        }

        private void addVertex(IVertex<VertexAttrs, EdgeAttrs> vertex)
        {
            var presenter = new Node { Content = vertex, DataContext = vertex };
            presenter.SetBinding(Canvas.LeftProperty, new Binding("Data.Left"));
            presenter.SetBinding(Canvas.TopProperty, new Binding("Data.Top"));
            presenter.SetBinding(Canvas.ZIndexProperty, new Binding("Data.ZIndex"));
            presenter.SetBinding(Node.IsSelectedProperty, new Binding("Data.IsSelected"));
            presenter.SetBinding(Node.IsHighlightedProperty, new Binding("Data.IsHighlighted"));

            _graphItems.Children.Add(presenter);
            // Update desired size of new UIElement.
            presenter.UpdateLayout();
            // Update properties of vertex model.
            vertex.Data.Width = presenter.DesiredSize.Width;
            vertex.Data.Height = presenter.DesiredSize.Height;
            _elementMap.Add(vertex, presenter);
        }

        private void delItem(object item)
        {
            FrameworkElement elt;
            if (_elementMap.TryGetValue(item, out elt))
                _graphItems.Children.Remove(elt);
        }

        private void fillVerteces()
        {
            if (_graphItems == null || Graph == null)
                return;
            foreach (var vertex in Graph.Verteces)
                addVertex(vertex);
        }

        private void fillEdges()
        {
            if (_graphItems == null || Graph == null)
                return;
            foreach (var vertex in Graph.Edges)
                addEdge(vertex);
        }

        private void clearAllItems()
        {
            if (_graphItems == null || Graph == null)
                return;
            _graphItems.Children.Clear();
            _elementMap.Clear();
        }

        private T getTemplateChild<T>(string childName)
            where T : DependencyObject
        {
            var child = (T)GetTemplateChild(childName);
            if (child == null)
                throw new ArgumentOutOfRangeException(string.Format("Template does not contain child <{0}>", childName));
            return child;
        }

        #endregion
    }
}